/*
 * Zmanim Java API
 * Copyright (C) 2004-2007 Eliyahu Hershfeld
 * 
 * This program is free software; you can redistribute it and/or modify it under the terms of the
 * GNU General Public License as published by the Free Software Foundation; either version 2 of the
 * License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without
 * even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License along with this program; if
 * not, write to the Free Software Foundation, Inc. 59 Temple Place - Suite 330, Boston, MA
 * 02111-1307, USA or connect to: http://www.fsf.org/copyleft/gpl.html
 */
package net.sourceforge.zmanim.util;

import java.lang.reflect.Method;
import java.text.DateFormat;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Date;
import java.util.Calendar;
import java.util.List;
import java.text.SimpleDateFormat;

import net.sourceforge.zmanim.Calendar.AstronomicalCalendar;


/**
 * A class used to format non {@link java.util.Date} times generated by the
 * Zmanim package. For example the
 * {@link net.sourceforge.zmanim.Calendar.AstronomicalCalendar#getTemporalHour()} returns
 * the length of the hour in milliseconds. This class can format this time.
 *
 * @author &copy; Eliyahu Hershfeld 2004 - 2007
 * @version 0.9.0
 */
public class ZmanimFormatter {
    private boolean prependZeroHours;

    private boolean useSeconds;

    private boolean useMillis;

    boolean useDecimal;

    private static DecimalFormat minuteSecondNF = new DecimalFormat("00");
    ;

    private DecimalFormat hourNF;

    private static DecimalFormat milliNF = new DecimalFormat("000");

    private SimpleDateFormat dateFormat;

    // private DecimalFormat decimalNF;

    /**
     * Format using hours, minutes, seconds and milliseconds using the xsd:time
     * format. This format will return 00.00.00.0 when formatting 0.
     */
    public static final int SEXAGESIMAL_XSD_FORMAT = 0;

    private int timeFormat = SEXAGESIMAL_XSD_FORMAT;

    /**
     * Format using standard decimal format with 5 positions after the decimal.
     */
    public static final int DECIMAL_FORMAT = 1;

    /**
     * Format using hours and minutes.
     */
    public static final int SEXAGESIMAL_FORMAT = 2;

    /**
     * Format using hours, minutes and seconds.
     */
    public static final int SEXAGESIMAL_SECONDS_FORMAT = 3;

    /**
     * Format using hours, minutes, seconds and milliseconds.
     */
    public static final int SEXAGESIMAL_MILLIS_FORMAT = 4;

    /**
     * constant for milliseconds in a minute (60,000)
     */
    static final long MINUTE_MILLIS = 60 * 1000;

    /**
     * constant for milliseconds in an hour (3,600,000)
     */
    static final long HOUR_MILLIS = MINUTE_MILLIS * 60;

    /**
     * Format using the XSD Duration format. This is in the format of
     * PT1H6M7.869S (P for period (duration), T for time, H, M and S indicate
     * hours, minutes and seconds.
     */
    public static final int XSD_DURATION_FORMAT = 5;

    public ZmanimFormatter() {
        this(0, new SimpleDateFormat("h:mm:ss"));
    }

    /**
     * ZmanimFormatter constructor using a formatter
     *
     * @param format int The formatting style to use. Using
     *               ZmanimFormatter.SEXAGESIMAL_SECONDS_FORMAT will format the
     *               time time of 90*60*1000 + 1 as 1:30:00
     */
    public ZmanimFormatter(int format, SimpleDateFormat dateFormat) {
        String hourFormat = "0";
        if (prependZeroHours) {
            hourFormat = "00";
        }
        hourNF = new DecimalFormat(hourFormat);
        // decimalNF = new DecimalFormat("0.0####");
        setTimeFormat(format);
        this.setDateFormat(dateFormat);
    }

    /**
     * Sets the format to use for formatting.
     *
     * @param format int the format constant to use.
     */
    public void setTimeFormat(int format) {
        timeFormat = format;
        switch (format) {
            case SEXAGESIMAL_XSD_FORMAT:
                setSettings(true, true, true);
                break;
            case SEXAGESIMAL_FORMAT:
                setSettings(false, false, false);
                break;
            case SEXAGESIMAL_SECONDS_FORMAT:
                setSettings(false, true, false);
                break;
            case SEXAGESIMAL_MILLIS_FORMAT:
                setSettings(false, true, true);
                break;
            case DECIMAL_FORMAT:
            default:
                useDecimal = true;
        }
    }

    public void setDateFormat(SimpleDateFormat sdf) {
        dateFormat = sdf;
    }

    public SimpleDateFormat getDateFormat() {
        return dateFormat;
    }

    private void setSettings(boolean prependZeroHours, boolean useSeconds,
                             boolean useMillis) {
        this.prependZeroHours = prependZeroHours;
        this.useSeconds = useSeconds;
        this.useMillis = useMillis;
    }

    /**
     * A method that formats milliseconds into a time format.
     *
     * @param milliseconds The time in milliseconds.
     * @return String The formatted <code>String</code>
     */
    public String format(double milliseconds) {
        return format((int) milliseconds);
    }

    /**
     * A method that formats milliseconds into a time format.
     *
     * @param millis The time in milliseconds.
     * @return String The formatted <code>String</code>
     */
    public String format(int millis) {
        return format(new Time(millis));
    }

    /**
     * A method that formats {@link Time}objects.
     *
     * @param time The time <code>Object</code> to be formatted.
     * @return String The formatted <code>String</code>
     */
    public String format(Time time) {
        if (timeFormat == XSD_DURATION_FORMAT) {
            return formatXSDDurationTime(time);
        }
        StringBuffer sb = new StringBuffer();
        sb.append(hourNF.format(time.getHours()));
        sb.append(":");
        sb.append(minuteSecondNF.format(time.getMinutes()));
        if (useSeconds) {
            sb.append(":");
            sb.append(minuteSecondNF.format(time.getSeconds()));
        }
        if (useMillis) {
            sb.append(".");
            sb.append(milliNF.format(time.getMilliseconds()));
        }
        return sb.toString();
    }

    /**
     * Formats a date using this classe's {@link #getDateFormat() date format}.
     *
     * @param dateTime the date to format
     * @param calendar the {@link java.util.Calendar Calendar} used to help format
     *                 based on the Calendar's DST and other settings.
     * @return the formatted String
     */
    public String formatDateTime(Date dateTime, Calendar calendar) {
        dateFormat.setCalendar(calendar);
        if (this.dateFormat.toPattern().equals("yyyy-MM-dd'T'HH:mm:ss")) {
            return getXSDateTime(dateTime, calendar);
        } else {
            return dateFormat.format(dateTime);
        }

    }

    /**
     * The date:date-time function returns the current date and time as a
     * date/time string. The date/time string that's returned must be a string
     * in the format defined as the lexical representation of xs:dateTime in <a
     * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">[3.3.8 dateTime]</a>
     * of <a href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2:
     * Datatypes]</a>. The date/time format is basically CCYY-MM-DDThh:mm:ss,
     * although implementers should consult <a
     * href="http://www.w3.org/TR/xmlschema11-2/">[XML Schema 1.1 Part 2:
     * Datatypes]</a> and <a href="http://www.iso.ch/markete/8601.pdf">[ISO
     * 8601]</a> for details. The date/time string format must include a time
     * zone, either a Z to indicate Coordinated Universal Time or a + or -
     * followed by the difference between the difference from UTC represented as
     * hh:mm.
     */
    public String getXSDateTime(Date dateTime, Calendar cal) {
        String xsdDateTimeFormat = "yyyy-MM-dd'T'HH:mm:ss";
        /*
           * if (xmlDateFormat == null || xmlDateFormat.trim().equals("")) {
           * xmlDateFormat = xsdDateTimeFormat; }
           */
        SimpleDateFormat dateFormat = new SimpleDateFormat(xsdDateTimeFormat);

        StringBuffer buff = new StringBuffer(dateFormat.format(dateTime));
        // Must also include offset from UTF.
        // Get the offset (in milliseconds).
        int offset = cal.get(Calendar.ZONE_OFFSET)
                + cal.get(Calendar.DST_OFFSET);
        // If there is no offset, we have "Coordinated
        // Universal Time."
        if (offset == 0)
            buff.append("Z");
        else {
            // Convert milliseconds to hours and minutes
            int hrs = offset / (60 * 60 * 1000);
            // In a few cases, the time zone may be +/-hh:30.
            int min = offset % (60 * 60 * 1000);
            char posneg = hrs < 0 ? '-' : '+';
            buff.append(posneg + formatDigits(hrs) + ':' + formatDigits(min));
        }
        return buff.toString();
    }

    /**
     * Represent the hours and minutes with two-digit strings.
     *
     * @param digits hours or minutes.
     * @return two-digit String representation of hrs or minutes.
     */
    private static String formatDigits(int digits) {
        String dd = String.valueOf(Math.abs(digits));
        return dd.length() == 1 ? '0' + dd : dd;
    }

    /**
     * This returns the xml representation of an xsd:duration object.
     *
     * @param millis the duration in milliseconds
     * @return the xsd:duration formatted String
     */
    public String formatXSDDurationTime(long millis) {
        return formatXSDDurationTime(new Time(millis));
    }

    /**
     * This returns the xml representation of an xsd:duration object.
     *
     * @param time the duration as a Time object
     * @return the xsd:duration formatted String
     */
    public String formatXSDDurationTime(Time time) {
        StringBuffer duration = new StringBuffer();

        duration.append("P");

        if (time.getHours() != 0 || time.getMinutes() != 0
                || time.getSeconds() != 0 || time.getMilliseconds() != 0) {
            duration.append("T");

            if (time.getHours() != 0)
                duration.append(time.getHours() + "H");

            if (time.getMinutes() != 0)
                duration.append(time.getMinutes() + "M");

            if (time.getSeconds() != 0 || time.getMilliseconds() != 0) {
                duration.append(time.getSeconds() + "."
                        + milliNF.format(time.getMilliseconds()));
                duration.append("S");
            }
            if (duration.length() == 1) // zero seconds
                duration.append("T0S");
            if (time.isNegative())
                duration.insert(0, "-");
        }
        return duration.toString();
    }

    /**
     * A method that returns an XML formatted <code>String</code> representing
     * the serialized <code>Object</code>. The format used is:
     * <p/>
     * <pre>
     *  &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;net.sourceforge.zmanim.AstronomicalCalendar algorithm=&quot;US Naval Almanac Algorithm&quot; location=&quot;Lakewood, NJ&quot; latitude=&quot;40.095965&quot; longitude=&quot;-74.22213&quot; elevation=&quot;31.0&quot; timeZoneName=&quot;Eastern Standard Time&quot; timeZoneID=&quot;America/New_York&quot; timeZoneOffset=&quot;-5&quot;&gt;
     *     &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
     *     &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
     *     ...
     *   &lt;/AstronomicalTimes&gt;
     * </pre>
     * <p/>
     * Note that the output uses the <a
     * href="http://www.w3.org/TR/xmlschema11-2/#dateTime">xsd:dateTime</a>
     * format for times such as sunrise, and <a
     * href="http://www.w3.org/TR/xmlschema11-2/#duration">xsd:duration</a>
     * format for times that are a duration such as the length of a
     * {@link net.sourceforge.zmanim.Calendar.AstronomicalCalendar#getTemporalHour() temporal hour}. The output of this method is
     * returned by the {@link #toString() toString} }.
     *
     * @return The XML formatted <code>String</code>. The format will be:
     *         <p/>
     *         <pre>
     *                          &lt;AstronomicalTimes date=&quot;1969-02-08&quot; type=&quot;net.sourceforge.zmanim.AstronomicalCalendar algorithm=&quot;US Naval Almanac Algorithm&quot; location=&quot;Lakewood, NJ&quot; latitude=&quot;40.095965&quot; longitude=&quot;-74.22213&quot; elevation=&quot;31.0&quot; timeZoneName=&quot;Eastern Standard Time&quot; timeZoneID=&quot;America/New_York&quot; timeZoneOffset=&quot;-5&quot;&gt;
     *                             &lt;Sunrise&gt;2007-02-18T06:45:27-05:00&lt;/Sunrise&gt;
     *                             &lt;TemporalHour&gt;PT54M17.529S&lt;/TemporalHour&gt;
     *                             ...
     *                          &lt;/AstronomicalTimes&gt;
     *                         </pre>
     */
    public static String toXML(AstronomicalCalendar ac) {
        ZmanimFormatter formatter = new ZmanimFormatter(
                ZmanimFormatter.XSD_DURATION_FORMAT, new SimpleDateFormat(
                        "yyyy-MM-dd'T'HH:mm:ss"));
        DateFormat df = new SimpleDateFormat("yyyy-MM-dd");
        String output = "<";
        if (ac.getClass().getName().endsWith("AstronomicalCalendar")) {
            output += "AstronomicalTimes";
        } else if (ac.getClass().getName().endsWith("ZmanimCalendar")) {
            output += "Zmanim";
        }
        output += " date=\"" + df.format(ac.getCalendar().getTime()) + "\"";
        output += " type=\"" + ac.getClass().getName() + "\"";
        output += " algorithm=\""
                + ac.getCalculator().getCalculatorName() + "\"";
        output += " location=\"" + ac.getGeoLocation().getLocationName() + "\"";
        output += " latitude=\"" + ac.getGeoLocation().getLatitude() + "\"";
        output += " longitude=\"" + ac.getGeoLocation().getLongitude() + "\"";
        output += " elevation=\"" + ac.getGeoLocation().getElevation() + "\"";
        output += " timeZoneName=\""
                + ac.getGeoLocation().getTimeZone().getDisplayName() + "\"";
        output += " timeZoneID=\"" + ac.getGeoLocation().getTimeZone().getID()
                + "\"";
        output += " timeZoneOffset=\""
                + (ac.getGeoLocation().getTimeZone().getOffset(
                ac.getCalendar().getTimeInMillis()) / (HOUR_MILLIS))
                + "\"";

        output += ">\n";

        Method[] theMethods = ac.getClass().getMethods();
        String tagName = "";
        Object value = null;
        List dateList = new ArrayList();
        List durationList = new ArrayList();
        List otherList = new ArrayList();
        for (int i = 0; i < theMethods.length; i++) {
            if (includeMethod(theMethods[i])) {
                tagName = theMethods[i].getName().substring(3);
                //String returnType = theMethods[i].getReturnType().getName();
                try {
                    value = theMethods[i].invoke(ac, (Object[]) null);
                    if (value == null) {//FIXME: use reflection to determine what the return type is, not the value
                        otherList.add("<" + tagName + ">N/A</" + tagName + ">");
                    } else if (value instanceof Date) {
                        dateList.add(new Zman((Date) value, tagName));
                    } else if (value instanceof Long) {// shaah zmanis
                        durationList.add(new Zman((int) ((Long) value)
                                .longValue(), tagName));
                    } else { // will probably never enter this block, but is
                        // present to be future proof
                        otherList.add("<" + tagName + ">" + value + "</"
                                + tagName + ">");
                    }
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        Zman zman;
        Collections.sort(dateList, Zman.DATE_ORDER);
        for (int i = 0; i < dateList.size(); i++) {
            zman = (Zman) dateList.get(i);
            output += "\t<" + zman.getZmanLabel();
            output += ">";
            output += formatter.formatDateTime(zman.getZman(), ac.getCalendar())
                    + "</" + zman.getZmanLabel() + ">\n";
        }
        Collections.sort(durationList, Zman.DURATION_ORDER);
        for (int i = 0; i < durationList.size(); i++) {
            zman = (Zman) durationList.get(i);
            output += "\t<" + zman.getZmanLabel();
            output += ">";
            output += formatter.format((int) zman.getDuration()) + "</"
                    + zman.getZmanLabel() + ">\n";
        }

        for (int i = 0; i < otherList.size(); i++) {// will probably never enter
            // this block
            output += "\t" + otherList.get(i) + "\n";
        }

        if (ac.getClass().getName().endsWith("AstronomicalCalendar")) {
            output += "</AstronomicalTimes>";
        } else if (ac.getClass().getName().endsWith("ZmanimCalendar")) {
            output += "</Zmanim>";
        }
        return output;
    }

    /**
     * Determines if a method should be output by the {@link #toXML(net.sourceforge.zmanim.Calendar.AstronomicalCalendar)}
     *
     * @param method
     * @return
     */
    private static boolean includeMethod(Method method) {
        List methodWhiteList = new ArrayList();
        // methodWhiteList.add("getName");

        List methodBlackList = new ArrayList();
        // methodBlackList.add("getGregorianChange");

        if (methodWhiteList.contains(method.getName()))
            return true;
        if (methodBlackList.contains(method.getName()))
            return false;

        if (method.getParameterTypes().length > 0)
            return false; // Skip get methods with parameters since we do not
        // know what value to pass
        if (!method.getName().startsWith("get"))
            return false;

        if (method.getReturnType().getName().endsWith("Date")
                || method.getReturnType().getName().endsWith("long")) {
            return true;
        }
        return false;
    }
}